﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Razor;

namespace CodeRazor
{
    public class CodeGenerator
    {
        const string ModelDeclarationToken = "@model";
        private static readonly Regex languageByExtensionRegex = new Regex(@"^[.](\w+)html$", RegexOptions.IgnoreCase);
        private static readonly ISet<string> razorFileExtensions;

        static CodeGenerator()
        {
            razorFileExtensions = new HashSet<string>(CSharpRazorCodeLanguage.Languages.Keys.Select(l => string.Concat(".", l).ToUpperInvariant()));
        }

        public void ParseActiveProject()
        {
            var projectManager = ProjectManager.FromActiveSolution();
            var project = projectManager.GetActiveProject();
            this.ParseProject(projectManager, project);
        }

        public void ParseProject(string projectName)
        {
            var projectManager = ProjectManager.FromActiveSolution();
            var project = projectManager.GetProjectByName(projectName);
            this.ParseProject(projectManager, project);
        }

        private void ParseProject(ProjectManager projectManager, EnvDTE.Project project)
        {
            if (project == null)
            {
                throw new ArgumentException("Could not locate project");
            }

            foreach (FileInfo razorFile in projectManager.GetFilesInProject(project, IsRazorFile))
            {
                Action<FileInfo, string> addFileToProject = (outputFile, contents) => projectManager.AddFileToProject(project, razorFile, outputFile, contents);
                this.ParseToFile(razorFile, GetCodeFile(razorFile), addFileToProject);
            }
        }

        public void ParseToFile(FileInfo razorFile)
        {
            this.ParseToFile(razorFile, GetCodeFile(razorFile));
        }

        public void ParseToFile(FileInfo razorFile, FileInfo outputFile)
        {
            this.ParseToFile(razorFile, outputFile, null);
        }

        private void ParseToFile(FileInfo razorFile, FileInfo outputFile, Action<FileInfo, string> addFileToProject)
        {
            if (outputFile == null)
            {
                throw new ArgumentNullException("outputFile");
            }

            string newContents = this.Parse(razorFile);
            bool overwriteOutputFile = true;

            if (outputFile.Exists)
            {
                string oldContents = File.ReadAllText(outputFile.FullName);
                overwriteOutputFile = !StringComparer.Ordinal.Equals(newContents, oldContents);
            }

            if (overwriteOutputFile)
            {
                if (addFileToProject == null)
                {
                    File.WriteAllText(outputFile.FullName, newContents);
                }
                else
                {
                    addFileToProject(outputFile, newContents);
                }
            }
        }

        public string Parse(FileInfo razorFile)
        {
            using (var output = new StringWriter())
            {
                this.Parse(razorFile, output);
                output.Flush();
                return output.ToString();
            }
        }

        public void Parse(FileInfo razorFile, TextWriter output)
        {
            if (razorFile == null)
            {
                throw new ArgumentNullException("razorFile");
            }
            else if (!razorFile.Exists)
            {
                throw new IOException(string.Format("The Razor file does not exist: '{0}'", razorFile.FullName));
            }
            else if (output == null)
            {
                throw new ArgumentNullException("output");
            }

            using (var codeProvider = CreateCodeDomProvider(razorFile))
            {
                var compileUnit = this.CreateCompileUnit(codeProvider, razorFile);
                var generatorOptions = new CodeGeneratorOptions{ BlankLinesBetweenMembers = false, BracingStyle = "C", IndentString = "    "};
                codeProvider.GenerateCodeFromCompileUnit(compileUnit, output, generatorOptions);
            }
        }

        private CodeCompileUnit CreateCompileUnit(CodeDomProvider codeProvider, FileInfo razorFile)
        {
            GeneratorResults generatorResults = null;

            using (var reader = new StringReader(GetContentsWithoutModelDeclaration(razorFile)))
            {
                string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(razorFile.Name);
                string className = codeProvider.CreateEscapedIdentifier(string.Format("_Page_Views_{0}__{1}_{2}", razorFile.Directory.Name, fileNameWithoutExtension, razorFile.Extension.Substring(1).ToLower()));
                var engine = CreateRazorTemplateEngine(razorFile);
                generatorResults = engine.GenerateCode(reader, className, "Implement.Namespace.Later", fileNameWithoutExtension + codeProvider.FileExtension);
            }

            if (generatorResults.ParserErrors.Any())
            {
                string errorMessage = string.Join(Environment.NewLine, generatorResults.ParserErrors.Select(e => string.Concat("Line: ", e.Length, Environment.NewLine, e.Message)));
                throw new InvalidOperationException(errorMessage);
            }

            DecorateCode(RemoveLinePragmasInExecuteMethod(generatorResults.GeneratedCode));
            return generatorResults.GeneratedCode;
        }

        private CodeCompileUnit RemoveLinePragmasInExecuteMethod(CodeCompileUnit code)
        {
            foreach (var method in code.Namespaces[0].Types[0].Members.OfType<CodeMemberMethod>())
            {
                if (StringComparer.Ordinal.Equals("Execute", method.Name))
                {
                    foreach (CodeStatement statement in method.Statements)
                    {
                        statement.LinePragma = null;
                    }

                    break;
                }
            }

            return code;
        }

        private CodeCompileUnit DecorateCode(CodeCompileUnit code)
        {
            var version = FileVersionInfo.GetVersionInfo(typeof(Program).Assembly.Location);
            var typeReference = code.Namespaces[0].Types[0];

            typeReference.CustomAttributes.Add(
                new CodeAttributeDeclaration(
                    new CodeTypeReference(typeof(GeneratedCodeAttribute)),
                    new CodeAttributeArgument(new CodePrimitiveExpression(version.ProductName)),
                    new CodeAttributeArgument(new CodePrimitiveExpression(version.FileVersion))));

            typeReference.CustomAttributes.Add(
                new CodeAttributeDeclaration(
                    new CodeTypeReference(typeof(System.Web.WebPages.PageVirtualPathAttribute)),
                    new CodeAttributeArgument(new CodePrimitiveExpression("~/Todo/Path/To/View.cshtml"))));

            var applicationInstanceProperty = new CodeMemberProperty
            {
                Attributes = MemberAttributes.Family | MemberAttributes.Final,
                Name = "ApplicationInstance",
                HasGet = true,
                Type = new CodeTypeReference(typeof(System.Web.HttpApplication))
            };
            applicationInstanceProperty.GetStatements.Add(
                new CodeMethodReturnStatement(
                    new CodeCastExpression(
                        new CodeTypeReference(typeof(System.Web.HttpApplication)),
                        new CodePropertyReferenceExpression(new CodeVariableReferenceExpression("Context"), "ApplicationInstance")))
            );
            typeReference.Members.Add(applicationInstanceProperty);

            return code;
        }

        private static CodeDomProvider CreateCodeDomProvider(FileInfo razorFile)
        {
            Match match = languageByExtensionRegex.Match(razorFile.Extension);

            if (match.Success)
            {
                return CodeDomProvider.CreateProvider(match.Groups[1].Value);
            }

            throw new NotSupportedException("The file type is not supported: " + razorFile.Name);
        }

        private static FileInfo GetCodeFile(FileInfo razorFile)
        {
            using (var codeProvider = CreateCodeDomProvider(razorFile))
            {
                string codeFileName = string.Concat(Path.GetFileNameWithoutExtension(razorFile.Name), ".", codeProvider.FileExtension);
                return new FileInfo(Path.Combine(razorFile.DirectoryName, codeFileName));
            }
        }

        private static bool IsRazorFile(FileInfo file)
        {
            return razorFileExtensions.Contains(file.Extension.ToUpperInvariant());
        }

        private static string GetContentsWithoutModelDeclaration(FileInfo razorFile)
        {
            StringBuilder buf = new StringBuilder();
            bool processedFirstLine = false;

            foreach (string line in File.ReadLines(razorFile.FullName))
            {
                if (!processedFirstLine)
                {
                    processedFirstLine = true;

                    if (!line.TrimStart().StartsWith(ModelDeclarationToken))
                    {
                        buf.AppendLine(line);
                    }
                }
                else
                {
                    buf.AppendLine(line);
                }
            }

            return buf.ToString();
        }

        private static string GetViewBaseTypeName(FileInfo razorFile)
        {
            return File.ReadLines(razorFile.FullName)
                .Take(1)
                .Where(line => line.TrimStart().StartsWith(ModelDeclarationToken))
                .Select(line => line.Replace(ModelDeclarationToken, "").Trim())
                .Select(modelType => string.Format("System.Web.Mvc.WebViewPage<{0}>", modelType))
                .FirstOrDefault(line => !string.IsNullOrEmpty(line)) ?? "System.Web.Mvc.WebViewPage<dynamic>";
        }

        private static RazorTemplateEngine CreateRazorTemplateEngine(FileInfo razorFile)
        {
            var host = new RazorEngineHost(RazorCodeLanguage.GetLanguageByExtension(razorFile.Extension))
            {
                DefaultBaseClass = GetViewBaseTypeName(razorFile)
            };
            
            host.NamespaceImports.Add("System");
            host.NamespaceImports.Add("System.Collections.Generic");
            host.NamespaceImports.Add("System.IO");
            host.NamespaceImports.Add("System.Linq");
            host.NamespaceImports.Add("System.Net");
            host.NamespaceImports.Add("System.Web");
            host.NamespaceImports.Add("System.Web.Helpers");
            host.NamespaceImports.Add("System.Web.Security");
            host.NamespaceImports.Add("System.Web.UI");
            host.NamespaceImports.Add("System.Web.WebPages");
            host.NamespaceImports.Add("System.Web.Mvc");
            host.NamespaceImports.Add("System.Web.Mvc.Ajax");
            host.NamespaceImports.Add("System.Web.Mvc.Html");
            host.NamespaceImports.Add("System.Web.Routing");
            host.NamespaceImports.Add("System.Collections");
            host.NamespaceImports.Add("System.Collections.Specialized");
            host.NamespaceImports.Add("System.ComponentModel.DataAnnotations");
            host.NamespaceImports.Add("System.Configuration");
            host.NamespaceImports.Add("System.Text");
            host.NamespaceImports.Add("System.Text.RegularExpressions");
            host.NamespaceImports.Add("System.Web.Caching");
            host.NamespaceImports.Add("System.Web.DynamicData");
            host.NamespaceImports.Add("System.Web.SessionState");
            host.NamespaceImports.Add("System.Web.Profile");
            host.NamespaceImports.Add("System.Web.UI.WebControls");
            host.NamespaceImports.Add("System.Web.UI.WebControls.WebParts");
            host.NamespaceImports.Add("System.Web.UI.HtmlControls");
            host.NamespaceImports.Add("System.Xml.Linq");
            return new System.Web.Razor.RazorTemplateEngine(host);
        }
    }
}
